package edu.bbk.muc.lifetracker.common;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import android.location.Location;
import android.util.Log;

/**
 * Class {@link GlobalEvent} will be accessed only from sensors.
 * 
 * In short, it's a singleton which provides setTimestamp/setLocation/setXXX methods, 
 * which, before setting the data, try to acquire a
 * read-level lock (see {@link #setLocation(Location)} and others). Locking is
 * needed for the case when several sensor listeners will go off roughly at the
 * same time, and at some point object may become inconsistent.
 * 
 * {@link GlobalEvent} can also tell if it has enough fields to build an
 * {@link Event} object (see {@link #isComplete(Configuration)},
 * {@link #buildEvent(Configuration)}).
 * 
 * @author irenatyler and romankirillov
 */
public class GlobalEvent {
  /**
   * One and only instance
   */
  private static final GlobalEvent globalEvent = new GlobalEvent();

  // 1 second
  private static final long TIMEOUT = (long) 2E9;
  private static final String TAG = "GlobalEvent";

  /**
   * Accessor method of singleton
   */
  public static GlobalEvent get() {
    return globalEvent;
  }

  // this group of fields will be used to build Event objects
  private SimpleLocation location;
  private Acceleration acceleration;
  private Float ambientLight;
  private Long timestamp;
  private Long firstEventTimestamp;

  // Locks
  private final ReentrantReadWriteLock rwLock;

  private final Lock readLock;

  private GlobalEvent() {
    rwLock = new ReentrantReadWriteLock();
    readLock = rwLock.readLock();
  }

  /**
   * Should be invoked only after successfully calling
   * {@link #isComplete(Configuration)}.
   * 
   * @param configuration
   *          current sensors configuration, can be the instance of whatever was
   *          passed from UI or any other instance implementing this interface
   * 
   * @return an {@link Event} object or <code>null</code> if event is incomplete
   *         for a given {@link Configuration}.
   */
  public Event buildEvent(Configuration configuration) {
    // in order to achieve always consistent state, we always acquire lock
    readLock.lock();

    if ((null == timestamp) || (firstEventTimestamp == null)) {
      // incomplete configuration, cannot build event
      readLock.unlock();
      return null;
    }

    Log.d(TAG, "buildEvent entered");
    if (timestamp.longValue() - firstEventTimestamp.longValue() <= TIMEOUT) {
      readLock.unlock();
      return null;
    }

    Log.d(TAG, "There was a timeout, continuing building event");

    // maybe we will need to replace it with some class which actually
    // implements Event, but this will do for now
    Event event =
        new Event(System.currentTimeMillis(), this.acceleration, this.ambientLight, this.location);
    // after we constructed event and it is ready to dispatch,
    // we set current fields to null

    timestamp = null;
    firstEventTimestamp = null;

    // don't forget to unlock the mutex!
    readLock.unlock();

    return event;
  }

  public Long getTimestamp() {
    return timestamp;
  }

  public void setAcceleration(long timestamp, Acceleration acceleration) {
    readLock.lock();
    this.acceleration = acceleration;
    setTimestamp(timestamp);
    readLock.unlock();
  }

  public void setAmbientLight(long timestamp, float light) {
    readLock.lock();
    this.ambientLight = light;
    setTimestamp(timestamp);
    readLock.unlock();
  }

  public void setLocation(long timestamp, SimpleLocation location) {
    readLock.lock();
    this.location = location;
    setTimestamp(timestamp);
    readLock.unlock();
  }

  public void setTimestamp(Long timestamp) {
    readLock.lock();
    this.timestamp = timestamp;
    readLock.unlock();
  }

  /**
   * Checks if event is complete considering passed {@link Configuration}
   * object. Can only be called from lock-controlled code, hence private
   */
  private boolean isComplete(Configuration configuration) {
    if (!configuration.isLoggingEnabled()) {
      throw new IllegalArgumentException(
          "Cannot call isComplete when logging is disabled. " +
              "This is creepy, stop freaking user out!");
    }

    boolean complete = true;
    if (configuration.isLocationEnabled()) {
      complete &= (location != null);
    }

    if (configuration.isAccelerationEnabled()) {
      complete &= (acceleration != null);
    }

    if (configuration.isAmbientLightEnabled()) {
      complete &= (ambientLight != null);
    }

    return complete;
  }

  private void setTimestamp(long timestamp) {
    this.timestamp = timestamp;
    if (null == firstEventTimestamp) {
      firstEventTimestamp = timestamp;
    }
  }
}
